﻿using Microsoft.Owin.Security;
using Sustainalytics.Auth.Common;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace Sustainalytics.OAuth.Services.Common.Client
{
    /// <summary>
    /// OAuth client.
    /// </summary>
    public class OAuthClient
    {
        // client used for communicating with token endpoint
        private readonly HttpClient _tokenClient;

        // client used for communicating with resource endpoint
        private readonly HttpClient _resourceClient;

        // object for protecting/unprotecting the authentication ticket
        private readonly ISecureDataFormat<AuthenticationTicket> _ticketDataFormat = new CustomAccessTokenFormat();

        // current authentication ticket
        private AuthenticationTicket _authenticationTicket;

        // access token
        private string _accessToken;

        // refresh token
        private string _refreshToken;

        // access token expiration date
        private DateTime? _accessTokenExpiration;

        // client ID
        private string _clientId;

        // client secret
        private string _clientSecret = null;

        // scope
        private string _scope = null;


        /// <summary>
        /// Ctor.
        /// </summary>
        /// <param name="tokenClient"> Http client used for communicating with token endpoint (provides OAuth operations). </param>
        /// <param name="resourceClient"> Http client used for communicating with resource endpoint. </param>
        /// <param name="clientId"> Client ID. </param>
        /// <param name="clientSecret"> Client secret. </param>
        /// <param name="scope"> Scope. </param>
        /// 
        public OAuthClient(
            HttpClient tokenClient,
            HttpClient resourceClient,
            string clientId,
            string clientSecret = null,
            string scope = null)
        {
            this._tokenClient = tokenClient;
            this._resourceClient = resourceClient;
            this._clientId = clientId;
            this._clientSecret = clientSecret;
            this._scope = scope;
        }

        /// <summary>
        /// Ctor.
        /// </summary>
        /// <param name="tokenUrl"> Token endpoint URL. </param>
        /// <param name="resourceUrl"> Resource endpoint URL. </param>
        /// <param name="clientId"> Client ID. </param>
        /// <param name="clientSecret"> Client secret. </param>
        /// <param name="scope"> Scope. </param>
        /// 
        public OAuthClient(
            string tokenUrl,
            string resourceUrl,
            string clientId,
            string clientSecret = null,
            string scope = null)
        {
            this._tokenClient = new HttpClient();
            this._tokenClient.BaseAddress = new Uri(tokenUrl);

            this._resourceClient = new HttpClient();
            this._resourceClient.BaseAddress = new Uri(resourceUrl);

            this._clientId = clientId;
            this._clientSecret = clientSecret;
            this._scope = scope;
        }


        /// <summary>
        /// Password Flow Authenticate.
        /// </summary>
        /// <param name="username"> Username. </param>
        /// <param name="password"> Password. </param>
        public async Task<OAuthTokenResponse> Authenticate(string username, string password)
        {
            // authenticate
            OAuthTokenResponse tokenRes = await PasswordFlow_PostRequestForTokenAsync(
                client: this._tokenClient,
                path: null,
                username: username,
                password: password,
                clientId: this._clientId,
                clientSecret: this._clientSecret,
                scope: this._scope);

            // load members from token response
            SetOAuthTokenResponse(tokenRes);

            return tokenRes;
        }


        /// <summary>
        /// Set authentication ticket.
        /// </summary>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <returns> OAuth client for chainability. </returns>
        public OAuthClient SetAuthenticationTicket(AuthenticationTicket ticket)
        {
            this._authenticationTicket = ticket;

            this._accessTokenExpiration =
                (ticket.Properties.AllowRefresh == null || ticket.Properties.AllowRefresh.Value) ?
                (ticket.Properties.ExpiresUtc.HasValue ? (DateTime?)ticket.Properties.ExpiresUtc.Value.UtcDateTime : null)
                : null;

            // get access tocken
            this._accessToken = this._ticketDataFormat.Protect(ticket);

            return this;
        }

        /// <summary>
        /// Set access token.
        /// </summary>
        /// <param name="accessToken"> Access token. </param>
        /// <returns> OAuth client for chainability. </returns>
        public OAuthClient SetAccessToken(string accessToken)
        {
            var ticket = this._ticketDataFormat.Unprotect(accessToken);
            this.SetAuthenticationTicket(ticket);

            return this;
        }

        /// <summary>
        /// Set refresh token.
        /// </summary>
        /// <param name="refreshToken"> Refresh token. </param>
        /// <returns> OAuth client. </returns>
        public OAuthClient SetRefreshToken(string refreshToken)
        {
            this._refreshToken = refreshToken;

            return this;
        }

        /// <summary>
        /// Sets the access token expiration.
        /// </summary>
        /// <param name="accessTokenExpiration"> Access token expiration. </param>
        /// <returns> OAuth client. </returns>
        public OAuthClient SetAccessTokenExpiration(DateTime? accessTokenExpiration)
        {
            this._accessTokenExpiration = accessTokenExpiration;

            return this;
        }

        /// <summary>
        /// Set OAuth token response.
        /// </summary>
        /// <param name="tokenRes"> OAuth token response. </param>
        public void SetOAuthTokenResponse(OAuthTokenResponse tokenRes)
        {
            if (tokenRes.IsError)
                return;

            this._refreshToken = tokenRes.RefreshToken;

            // load authentication ticket
            var ticket = tokenRes.GetAuthenticationTicket(this._ticketDataFormat);
            SetAuthenticationTicket(ticket);
        }

        /// <summary>
        /// Check whether current access token has expired.
        /// </summary>
        /// <returns> Access token expired and was refreshed. </returns>
        public async Task<bool> CheckAccessTokenExpiration()
        {
            // if the refresh token has expired then get a new 
            if (this._accessTokenExpiration.HasValue
                && this._accessTokenExpiration < DateTime.UtcNow
                && string.IsNullOrEmpty(this._refreshToken) == false)
            {
                // request a new access token
                OAuthTokenResponse tokenRes = await RefreshToken_PostRequestForTokenAsync(this._tokenClient, string.Empty, this._refreshToken, this._clientId, this._clientSecret, this._scope);

                // load response
                this.SetOAuthTokenResponse(tokenRes);

                return true;
            }
            else
            {
                return false;
            }
        }


        /// <summary>
        /// GET.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> GetAsync(string path)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // GET
            HttpResponseMessage res = await this._resourceClient.GetAsync(path);

            return res;
        }

        /// <summary>
        /// GET.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="cancellationToken"> Cancellation token. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> GetAsync(string path, CancellationToken cancellationToken)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // GET
            HttpResponseMessage res = await this._resourceClient.GetAsync(path, cancellationToken);

            return res;
        }

        /// <summary>
        /// POST.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="content"> POST data. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> PostAsync(string path, HttpContent content)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();
            
            _resourceClient.Timeout = new TimeSpan(1, 0, 0, 0);
            // add token to request
            
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // POST
            HttpResponseMessage res = await this._resourceClient.PostAsync(path, content);

            return res;
        }

        /// <summary>
        /// POST.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="content"> POST data. </param>
        /// <param name="cancellationToken"> Cancellation token. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> PostAsync(string path, HttpContent content, CancellationToken cancellationToken)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();
            
            int timeOutValue;

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            var timeOut = 3600;

            this._resourceClient.Timeout =
                TimeSpan.FromMilliseconds(timeOut);

            var timeOutSettingValue = ConfigurationManager.AppSettings["LRTMPostTimeOut"]; 

            if (timeOutSettingValue != null)
                if (int.TryParse(timeOutSettingValue, out timeOutValue))
                {
                    this._resourceClient.Timeout =
                        TimeSpan.FromMilliseconds(int.Parse(ConfigurationManager.AppSettings["LRTMPostTimeOut"]));
                }

            // POST
            HttpResponseMessage res = await this._resourceClient.PostAsync(path, content, cancellationToken);

            return res;
        }

        /// <summary>
        /// PUT.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="content"> PUT data. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> PutAsync(string path, HttpContent content)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // PUT
            HttpResponseMessage res = await this._resourceClient.PutAsync(path, content);

            return res;
        }

        /// <summary>
        /// PUT.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="content"> PUT data. </param>
        /// <param name="cancellationToken"> Cancellation token. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> PutAsync(string path, HttpContent content, CancellationToken cancellationToken)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // PUT
            HttpResponseMessage res = await this._resourceClient.PutAsync(path, content, cancellationToken);

            return res;
        }

        /// <summary>
        /// DELETE.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> DeleteAsync(string path)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // DELETE
            HttpResponseMessage res = await this._resourceClient.DeleteAsync(path);

            return res;
        }

        /// <summary>
        /// DELETE.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="cancellationToken"> Cancellation token. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> DeleteAsync(string path, CancellationToken cancellationToken)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // DELETE
            HttpResponseMessage res = await this._resourceClient.DeleteAsync(path, cancellationToken);

            return res;
        }

        /// <summary>
        /// PATCH.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="content"> PATCH data. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> PatchAsync(string path, HttpContent content)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // create request
            var req = new HttpRequestMessage(new HttpMethod("PATCH"), path);
            req.Content = content;

            // PUT
            HttpResponseMessage res = await this._resourceClient.SendAsync(req);

            return res;
        }

        /// <summary>
        /// PATCH.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="content"> PATCH data. </param>
        /// <param name="cancellationToken"> Cancellation token. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> PatchAsync(string path, HttpContent content, CancellationToken cancellationToken)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // create request
            var req = new HttpRequestMessage(new HttpMethod("PATCH"), path);
            req.Content = content;

            // PUT
            HttpResponseMessage res = await this._resourceClient.SendAsync(req, cancellationToken);

            return res;
        }

        /// <summary>
        /// HTTP METHOD.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="method"> HTTP method. </param>
        /// <param name="content"> Content. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> SendAsync(string path, HttpMethod method, HttpContent content = null)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // create request
            var req = new HttpRequestMessage(method, path);

            if (content != null)
            {
                req.Content = content;
            }

            // PUT
            HttpResponseMessage res = await this._resourceClient.SendAsync(req);

            return res;
        }

        /// <summary>
        /// HTTP METHOD.
        /// </summary>
        /// <param name="path"> Path. </param>
        /// <param name="method"> HTTP method. </param>
        /// <param name="cancellationToken"> Cancellation token. </param>
        /// <param name="content"> Content. </param>
        /// <returns> Response. </returns>
        public async Task<HttpResponseMessage> SendAsync(string path, HttpMethod method, CancellationToken cancellationToken, HttpContent content = null)
        {
            // check if the access token has expired
            await CheckAccessTokenExpiration();

            // add token to request
            AddTokenToRequest(this._resourceClient, this._accessToken);

            // create request
            var req = new HttpRequestMessage(method, path);

            if (content != null)
            {
                req.Content = content;
            }

            // PUT
            HttpResponseMessage res = await this._resourceClient.SendAsync(req, cancellationToken);

            return res;
        }


        /// <summary>
        /// Post request to obtain OAuth token.
        /// Password Flow (Resource Owner Password Credentials Grant).
        /// </summary>
        /// <param name="client"> Http client. </param>
        /// <param name="path"> Token URL path. </param>
        /// <param name="username"> Username. </param>
        /// <param name="password"> Password. </param>
        /// <param name="clientId"> Client ID. </param>
        /// <param name="clientSecret"> Client Secret. </param>
        /// <param name="scope"> Scope. </param>
        /// <returns> Access Token. </returns>
        private static async Task<OAuthTokenResponse> PasswordFlow_PostRequestForTokenAsync(HttpClient client, string path, string username, string password, string clientId, string clientSecret = null, string scope = null)
        {
            Dictionary<string, string> data = new Dictionary<string, string>
            {
                { "grant_type", "password" },
                { "username", username },
                { "password", password },
                { "client_id", clientId }
            };

            if (string.IsNullOrEmpty(clientSecret) == false)
            {
                data.Add("client_secret", clientSecret);
            }

            if (string.IsNullOrEmpty(scope) == false)
            {
                data.Add("scope", scope);
            }

            var token = await PostRequestForTokenAsync(client, path, data);

            return token;
        }

        private static async Task<OAuthTokenResponse> RefreshToken_PostRequestForTokenAsync(HttpClient client, string path, string refreshToken, string clientId, string clientSecret = null, string scope = null)
        {
            Dictionary<string, string> data = new Dictionary<string, string>
            {
                { "grant_type", "refresh_token" },
                { "client_id", clientId },
                { "refresh_token", refreshToken }
            };

            if (string.IsNullOrEmpty(clientSecret) == false)
            {
                data.Add("client_secret", clientSecret);
            }

            if (string.IsNullOrEmpty(scope) == false)
            {
                data.Add("scope", scope);
            }

            var token = await PostRequestForTokenAsync(client, path, data);

            return token;
        }

        /// <summary>
        /// Post request to obtain OAuth token.
        /// </summary>
        /// <param name="client"> Http client. </param>
        /// <param name="path"> Token URL path. </param>
        /// <param name="data"> Request data. </param>
        /// <returns> Token. </returns>
        private static async Task<OAuthTokenResponse> PostRequestForTokenAsync(HttpClient client, string path, IDictionary<string, string> data)
        {
            // send token request
            var response = await client.PostAsync(path, new FormUrlEncodedContent(data));

            // receive token
            var content = await response.Content.ReadAsStringAsync();

            if (string.IsNullOrEmpty(content))
            {
                throw new InvalidTokenResponseException(content);
            }
            else
            {
                OAuthTokenResponse token = new OAuthTokenResponse(content);
                return token;
            }
        }

        /// <summary>
        /// Add authorization token.
        /// </summary>
        /// <param name="client"> Http client. </param>
        /// <param name="token"> OAuth token. </param>
        private static void AddTokenToRequest(HttpClient client, string token)
        {
            // add authorization header
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
    }
}
